%%------------------------------------------------------------------------------
% @doc kvcache_worker module
% @author hgx <hgx@live.cn>
% @copyright 2024 hgx, All rights reserved.
% @since 2024-05-21 
%%------------------------------------------------------------------------------
-module(kvcache_worker).
-behaviour(gen_server).

-include_lib("eunit/include/eunit.hrl").
-record(state, {timeout = 0, timestamp = 0}).

-export([start_link/1]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).


%% public functions
start_link(Hour) ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, Hour, []).



%%------------------------------------------------------------------------------
%% gen_server safe function
do_init(Hour) ->
    ets:new(?MODULE, [{keypos, 1}, named_table, public, set, {read_concurrency, true}]),

    {ok, #state{ timeout    = erlang:ceil(Hour * 60 * 60) 
               , timestamp  = erlang:system_time(second)
               }}.


%%------------------------------------------------------------------------------
do_call(show, _From, State) ->
    Fields = record_info(fields, state),
    Values = tl(tuple_to_list(State)),
    {reply, lists:zip(Fields, Values), State};

do_call(_Info, _From, State) ->
    {reply, undefined, State}.


%%------------------------------------------------------------------------------
do_cast({refresh, Key}, State) ->
    Timeout = State#state.timeout + State#state.timestamp,
    do_refresh(Key, Timeout),
    {noreply, State};

do_cast({set_kv, Key, Value}, State) ->
    Timeout = State#state.timeout + State#state.timestamp,
    do_set_kv(Key, Value, Timeout),
    {noreply, State};

do_cast(clean_all, State) ->
    ets:delete_all_objects(?MODULE),
    safe_erase(),
    {noreply, State};

%% 刷新时间戳，并清理过期数据
do_cast(timecallback, State) ->
    TS = erlang:system_time(second),
    do_clean_expired(TS),
    {noreply, State#state{timestamp = TS}};

do_cast({set_timeout, Hour}, State) ->
    Timeout = erlang:ceil(Hour * 60 * 60),
    {noreply, State#state{timeout = Timeout}};

do_cast(_Info, State) ->
    {noreply, State}.


%%------------------------------------------------------------------------------
do_info(_Info, State) ->
    {noreply, State}.


%%------------------------------------------------------------------------------
%% 加入缓存
do_set_kv(Key, Value, Timeout) ->
    ets:insert(?MODULE, {Key, Value}),
    do_refresh(Key, Timeout).


%% 刷新过期时间
do_refresh(Key, Timeout) ->
    erlang:put(Key, Timeout).


%% 清理过期的缓存
do_clean_expired(TS) ->
    List = erlang:get(),
    do_clean_expired(TS, List).

do_clean_expired(TS, [{'$initial_call', _T} | List])->
    do_clean_expired(TS, List);
do_clean_expired(TS, [{'$ancestors', _T} | List])->
    do_clean_expired(TS, List);
do_clean_expired(TS, [{K, T} | List]) when TS > T ->
    ets:delete(?MODULE, K),
    erlang:erase(K),
    do_clean_expired(TS, List);
do_clean_expired(TS, [_ | List]) -> 
    do_clean_expired(TS, List);
do_clean_expired(_ST, []) ->
    ok.


%% 清理所有字典数据，保留系统必要的
safe_erase() ->
    safe_erase( erlang:erase() ).

safe_erase([{'$initial_call', Data} | List]) ->
    erlang:put('$initial_call', Data),
    safe_erase(List);
safe_erase([{'$ancestors', Data} | List]) ->
    erlang:put('$ancestors', Data),
    safe_erase(List);
safe_erase([_ | List]) ->
    safe_erase(List);
safe_erase([]) ->
    ok.


%%------------------------------------------------------------------------------
%% gen_server base function
init(Value) ->
    try do_init(Value)
    catch
        Class:Reason:Stack ->
            ?debugFmt("init error, value: ~p error:~n~p", [Value, {Class, Reason, Stack}]),
            {stop, Reason}
    end.

handle_call(Info, From, State) ->
    try do_call(Info, From, State)
    catch
        Class:Reason:Stack ->
            ?debugFmt("handle_call error, Info: ~p ~nstate: ~n~p ~nerror:~p", [Info, State, {Class, Reason, Stack}]),
            {reply, undefined, State}
    end.

handle_cast(Info, State) ->
    try do_cast(Info, State)
    catch
        Class:Reason:Stack ->
            ?debugFmt("handle_cast error, Info: ~p ~nstate: ~n~p ~nerror:~p", [Info, State, {Class, Reason, Stack}]),
            {noreply, State}
    end.
    

handle_info(Info, State) ->
    try do_info(Info, State)
    catch
        Class:Reason:Stack ->
            ?debugFmt("handle_info error, Info: ~p ~nstate: ~n~p ~nerror:~p", [Info, State, {Class, Reason, Stack}]),
            {noreply, State}
    end.

terminate(_Reason, _Status) ->
    ok.

code_change(_Oldvsn, Status, _Extra) ->
    {ok, Status}.

